Python:Pygame Pong With Framework
=Pong!= In the last part, we created a framework for games. But what's the point of having a framework without showing how to use it? In true game-programming-tutorial fashion, I shall now create Pong! Another Word About Style Like my framework example, this will use more than one file, and they'll each be pretty big. So as to avoid a complete code dump right in the middle of the article, I'll be partitioning it up as follows: example.py if __name__ '__main__': print "Hello, world!" And later, when we add code to that: example.py def hiWorld(): print "Hello, world!" Sometimes, a method or class will be too freaking huge for me to put in one place. Python's pretty sensitive to indentation, so I'll try not to do this, but at times it makes for far more clear code. In theory, you should be able to just stitch the code together. example.py (reallyLongFunction, part 1) def reallyLongFunction(): print "This is a really really", example.py (reallyLongFunction, part 2) print "really really REALLY long function" Finally, the code dump: example.py (entire) def reallyLongFunction(): print "This is a really really", print "really really REALLY long function" def hiWorld(): print "Hello, world!" if __name__ '__main__': hiWorld() =The Title Screen= What's a game without a title screen? Plus, it's a simple way to illustrate the state mechanism. Setup Before we go about making our own state, we'll have to do a few things first - namely, we'll be setting up pygame and our screens. Note that, in order for this code to work, you'll have to have a pygame with TTF support compiled in. pong.py #!/usr/bin/env python import states import pygame from pygame.constants import * def main(): pygame.init() pygame.font.init() screen = pygame.display.set_mode( (640,480), DOUBLEBUF) driver = states.StateDriver(screen) title = TitleScreen(driver,screen) driver.start(title) driver.run() Four lines in that code deal with our framework: the driver constructor, the title screen's constructor, the driver.start() call which tells our statedriver what state it'll begin with, and finally the driver.run() call which kicks everything off. TitleScreen The title screen isn't anything fancy, it's mainly just drawing some text to the screen: pong.py (TitleScreen, part 1) class TitleScreen(states.State): def __init__(self,driver,screen): states.State.__init__(self,driver,screen) self.pongFont = pygame.font.Font(None,92) self.font = pygame.font.Font(None, 16) def paint(self,screen): white = (255, 255, 255) w,h = screen.get_size() surface = self.pongFont.render("PyPong!",0, white) centerX = w/2 - surface.get_width()/2 centerY = h*0.25 - surface.get_height()/2 screen.blit(surface, (centerX,centerY)) surface = self.font.render("A tutorial",0,(128,128,128)) centerX = w/2 - surface.get_width()/2 centerY = h/2 - surface.get_height()/2 screen.blit(surface, (centerX, centerY)) surface = self.font.render("Press any key to begin", 0, white) centerX = w/2 - surface.get_width()/2 centerY = h*0.75 - surface.get_height()/2 screen.blit(surface, (centerX, centerY)) "Now hold on!" I hear you saying, "The last thing the previous tutorial had us do was create a GuiState! And here we're not using it!" That's true. The reason here is that the title screen isn't really very dynamic at all - all that's needed is to write a few things up on it. Making the text separate Paintables and adding them to the scene, as well as registering keyables to listen for the keypress, would be a bit of overkill. Running With just a little more code, we can see our title screen in action: pong.py if __name__ '__main__': main() Make sure that's at the bottom of the pong.py file. Now, execute the code! If all's gone well, you should see your beautifully rendered title screen in all its splendor. Moving On While it's a nice title screen we have there, 'press a key and exit' is not exactly a working game concept. We need to have the title screen respond to key presses and begin the game. pong.py (TitleScreen, part 2) def keyEvent(self,key,unicode,pressed): if(pressed): playing = PlayingGameState(self._driver,self.screen) self._driver.replace(playing) In order for that to work, however, we need to import the next state. Put the import statement below at the top of pong.py with the rest of the imports: pong.py from playing import PlayingGameState =Playing= Before we get to the meat of playing the game, we first need to cover (yet more) basics. Score The most important thing about playing any game is winning! Thus, we'll need to make a way to keep track of the score for each side. Whereas before, in the title screen, making all the text a paintable was overkill, here it's exactly what we want. There are going to be two scores on the screen, after all. playing.py from states import * import gui import random import pygame from pygame.constants import * from done import GameOver class Score(gui.Paintable): def __init__(self,loc): gui.Paintable.__init__(self,loc) self.scoreFont = pygame.font.Font(None, 36) self.setScore(0) def setScore(self,score): self.score = score white = (255,255,255) self.scoreImage = self.scoreFont.render(str(score),0,white) def getScore(self): return self.score def paint(self,screen): if(self.scoreImage and self.loc): screen.blit(self.scoreImage, self.loc) Ball For the three of you on earth who have never heard of Pong, the object is to keep the ball from bouncing into your side. Our Ball is not only a paintable, but also updateable, since it'll need to move itself around the screen. playing.py class Ball(gui.Paintable, gui.Updateable): AXIS_X = 1 AXIS_Y = 2 def __init__(self,loc,bounds,radius=16,speed=110,increase=0.1): """The 'bounds' parameter indicates the width and height of the playing area""" gui.Paintable.__init__(self,loc) self.bounds = bounds self.radius = radius self.speed = speed self.increase = increase self.originalSpeed = speed self.dx = self.dy = 0 self.center() def bounce(self, axis): if(axis & self.AXIS_X): self.dx = -self.dx if(axis & self.AXIS_Y): self.dy = -self.dy self.speed = self.speed + self.speed * self.increase; def center(self): self.loc = [self.bounds0/2, self.bounds1/2] self.dx = random.choice((-1,1)) self.dy = random.choice((-1,1)) self.outOfBounds = 0 self.speed = self.originalSpeed def paint(self,screen): x = int(self.loc0) y = int(self.loc1) pygame.draw.circle(screen, (255,255,0), (x,y),self.radius) def update(self,delay): x,y = self.loc radius = self.radius toMove = delay * self.speed moveX = self.dx * toMove moveY = self.dy * toMove newX = x + moveX newY = y + moveY if(newY < radius or newY > self.bounds1 - radius): self.bounce(self.AXIS_Y) moveY = self.dy * toMove * 2 newY = y + moveY if(newX < radius): self.outOfBounds = -1 elif(newX > self.bounds0 - radius): self.outOfBounds = 1 self.loc0 = newX self.loc1 = newY Paddle Our game has matured from its humble beginnings as a title screen to a program which will bounce a ball around all day. It'd be nice to have some interaction: playing.py class Paddle(gui.Paintable, gui.Keyable, gui.Updateable): def __init__(self,loc,size,maxY,speed=125): gui.Paintable.__init__(self,loc) gui.Keyable.__init__(self, [ K_UP, K_DOWN ]) self.size = size self.maxY = maxY self.dy = 0 self.speed = speed self.center() def center(self): y = self.maxY / 2 - self.size1 / 2 self.loc = (self.loc0,y) def collidesWithBall(self,ball): topLeftX = self.loc0 - self.size0 / 2 topLeftY = self.loc1 - self.size1 / 2 width = self.size0 height = self.size1 ourRect = Rect(topLeftX,topLeftY,width,height) ballLeftX = ball.loc0 - ball.radius ballLeftY = ball.loc1 - ball.radius ballWidth = ball.radius * 2 ballHeight = ball.radius * 2 ballRect = Rect(ballLeftX,ballLeftY,ballWidth,ballHeight) if(ourRect.colliderect(ballRect)): ball.bounce(Ball.AXIS_X) return True return False def update(self,delay): x,y = self.loc halfHeight = self.size1/2 toMove = delay * self.speed moveY = self.dy * toMove newY = y + moveY if(newY < halfHeight or newY > self.maxY - halfHeight): return self.loc = (self.loc0,newY) def keyEvent(self,key,unicode, pressed): if(key K_UP): self.dy = -1 elif(key K_DOWN): self.dy = 1 if(not pressed): self.dy = 0 def paint(self,screen): topLeftX = self.loc0 - (self.size0 / 2) topLeftY = self.loc1 - (self.size1 / 2) rect = self.size[0, self.size1] pygame.draw.rect(screen, (255,255,255), rect) The Paddle uses just about every part of our GUI framework (the only exception is that it's not a Mouseable, because clicking on a Pong paddle isn't going to be very productive). It paints itself to the screen, it updates itself by moving whichever way it's set to, and it takes keypresses to change where it's moving to. AIPaddle We could stop right now and make the PlayingGameState - our pong would be a two-player game, where each player used different keys to move their paddle. The above code would need a few tweaks, but it's possible. However, it's not likely you're pair-programming during a tutorial, so it'd be somewhat difficult to test. Instead, let's make a computer opponent: playing.py class AIPaddle(Paddle): def __init__(self,loc,size,maxY,ball,speed=125): Paddle.__init__(self,loc,size,maxY,speed) self.ball = ball def keyEvent(self,key,unicode,pressed): pass def update(self,delay): Paddle.update(self,delay) if(self.ball.loc1 > self.loc1 + 5): self.dy = 1 elif(self.ball.loc1 < self.loc1 - 1): self.dy = -1 else: self.dy = 0 It's not the smartest - eventually the ball will be moving too fast for it to follow - but it's a decent enough opponent. Note that we had to override keyEvent here - otherwise the parent Paddle logic would have moved the computer's paddle whenever the player hit a key! PlayingGameState Most of the game logic is actually handled by the objects themselves - they know how to move, and the paddle can even check to see if a ball is hitting it and bounce it accordingly. class PlayingGameState(GuiState): def __init__(self,driver,screen): GuiState.__init__(self,driver,screen) self.ball = Ball(None, (640,480)) self.ball.center() self.add(self.ball) self.player1 = Paddle( (10,0), (15,75), 480) self.player1.center() self.add(self.player1) self.score1 = Score((20,5)) self.add(self.score1) self.player2 = AIPaddle( (630,0), (15,75), 480,self.ball) self.player2.center() self.add(self.player2) self.score2 = Score((610,5)) self.add(self.score2) def update(self,delay): GuiState.update(self,delay) self.player1.collidesWithBall(self.ball) self.player2.collidesWithBall(self.ball) score = 0 if(self.ball.outOfBounds < 0): score = self.score2.getScore() + 1 self.score2.setScore(score) elif(self.ball.outOfBounds > 0): score = self.score1.getScore() + 1 self.score1.setScore(score) if(score): self.ball.center() if(score >= 3): done = GameOver(self._driver,self.screen, self.score1,self.score2) self._driver.replace(done) As you can see, the parent GuiState and the Updateables themselves take care of a lot of the code - all our PlayingGameState had to do was add the objects, check for collisions, award points, and see if somebody won. =It's the End of the Game as we know it= The code just above here checks to see if anyone's achieved a score of 3 yet. If they have, it creates a new state and transitions to it. This is the last part of the game, where we'll display the final score and a victory or consolation message, as appropriate. GameOver GameOver is in many ways similar to TitleScreen, though it uses a bit more of the framework by borrowing PlayingGameState's score objects to display: done.py import pygame from pygame.constants import * from states import * class GameOver(State): def __init__(self,driver,screen,score1,score2): State.__init__(self,driver,screen) if(score1.getScore() > score2.getScore()): win = 1 else: win = 0 self.messageFont = pygame.font.Font(None,36) self.font = pygame.font.Font(None, 20) if(win): self.setMessage("You are victorious!") else: self.setMessage("You have lost!") self.score1 = score1 self.score2 = score2 def setMessage(self, message): self.message = message self.msgImage = self.messageFont.render(message, 0, (255,255,255)) def paint(self,screen): w = screen.get_width() h = screen.get_height() surface = self.msgImage centerX = w/2 - surface.get_width()/2 centerY = h*0.25 - surface.get_height()/2 screen.blit(surface, (centerX,centerY)) surface = self.messageFont.render("to",0,(255,255,255)) centerX = w/2 - surface.get_width()/2 centerY = h/2 - surface.get_height()/2 screen.blit(surface, (centerX,centerY)) self.score1.loc = 30,centerY self.score2.loc = 600,centerY self.score1.paint(screen) self.score2.paint(screen) That's all, folks! If everything's been entered correctly, you should have a fully operational game of pong at this point - run pong.py and find out! =Room for Improvement= As nice a framework as it is, it's just a beginning. There are many ways it could be improved: * Our framework uses a double-buffered surface and repaints it every time. Some games may find it more efficient to just re-paint the dirty rectangles. * Pygame has built-in sprite objects, as well as routines for creating groups and checking collisions between them - it might be useful to co-opt them into the framework * The screen is simply pygame's screen object - we could wrap this into a class of its own in case we wanted to use something else to render everything (for instance, if we wanted to give the user an option between pygame and opengl) =Code Dump, Part 2= pong.py #!/usr/bin/env python import states import pygame from pygame.constants import * from playing import PlayingGameState def main(): pygame.init() pygame.font.init() screen = pygame.display.set_mode( (640,480), DOUBLEBUF) driver = states.StateDriver(screen) title = TitleScreen(driver,screen) driver.start(title) driver.run() class TitleScreen(states.State): def __init__(self,driver,screen): states.State.__init__(self,driver,screen) self.pongFont = pygame.font.Font(None,92) self.font = pygame.font.Font(None, 16) def paint(self,screen): white = (255, 255, 255) w,h = screen.get_size() surface = self.pongFont.render("PyPong!",0, white) centerX = w/2 - surface.get_width()/2 centerY = h*0.25 - surface.get_height()/2 screen.blit(surface, (centerX,centerY)) surface = self.font.render("A tutorial",0,(128,128,128)) centerX = w/2 - surface.get_width()/2 centerY = h/2 - surface.get_height()/2 screen.blit(surface, (centerX, centerY)) surface = self.font.render("Press any key to begin", 0, white) centerX = w/2 - surface.get_width()/2 centerY = h*0.75 - surface.get_height()/2 screen.blit(surface, (centerX, centerY)) def keyEvent(self,key,unicode,pressed): if(pressed): playing = PlayingGameState(self._driver,self.screen) self._driver.replace(playing) if __name__ '__main__': main() playing.py from states import * import gui import random import pygame from pygame.constants import * from done import GameOver class Score(gui.Paintable): def __init__(self,loc): gui.Paintable.__init__(self,loc) self.scoreFont = pygame.font.Font(None, 36) self.setScore(0) def setScore(self,score): self.score = score white = (255,255,255) self.scoreImage = self.scoreFont.render(str(score),0,white) def getScore(self): return self.score def paint(self,screen): if(self.scoreImage and self.loc): screen.blit(self.scoreImage, self.loc) class Ball(gui.Paintable, gui.Updateable): AXIS_X = 1 AXIS_Y = 2 def __init__(self,loc,bounds,radius=16,speed=110,increase=0.1): """The 'bounds' parameter indicates the width and height of the playing area""" gui.Paintable.__init__(self,loc) self.bounds = bounds self.radius = radius self.speed = speed self.increase = increase self.originalSpeed = speed self.dx = self.dy = 0 self.center() def bounce(self, axis): if(axis & self.AXIS_X): self.dx = -self.dx if(axis & self.AXIS_Y): self.dy = -self.dy self.speed = self.speed + self.speed * self.increase; def center(self): self.loc = [self.bounds0/2, self.bounds1/2] self.dx = random.choice((-1,1)) self.dy = random.choice((-1,1)) self.outOfBounds = 0 self.speed = self.originalSpeed def paint(self,screen): x = int(self.loc0) y = int(self.loc1) pygame.draw.circle(screen, (255,255,0), (x,y),self.radius) def update(self,delay): x,y = self.loc radius = self.radius toMove = delay * self.speed moveX = self.dx * toMove moveY = self.dy * toMove newX = x + moveX newY = y + moveY if(newY < radius or newY > self.bounds1 - radius): self.bounce(self.AXIS_Y) moveY = self.dy * toMove * 2 newY = y + moveY if(newX < radius): self.outOfBounds = -1 elif(newX > self.bounds0 - radius): self.outOfBounds = 1 self.loc0 = newX self.loc1 = newY class Paddle(gui.Paintable, gui.Keyable, gui.Updateable): def __init__(self,loc,size,maxY,speed=125): gui.Paintable.__init__(self,loc) gui.Keyable.__init__(self, [ K_UP, K_DOWN ]) self.size = size self.maxY = maxY self.dy = 0 self.speed = speed self.center() def center(self): y = self.maxY / 2 - self.size1 / 2 self.loc = (self.loc0,y) def collidesWithBall(self,ball): topLeftX = self.loc0 - self.size0 / 2 topLeftY = self.loc1 - self.size1 / 2 width = self.size0 height = self.size1 ourRect = Rect(topLeftX,topLeftY,width,height) ballLeftX = ball.loc0 - ball.radius ballLeftY = ball.loc1 - ball.radius ballWidth = ball.radius * 2 ballHeight = ball.radius * 2 ballRect = Rect(ballLeftX,ballLeftY,ballWidth,ballHeight) if(ourRect.colliderect(ballRect)): ball.bounce(Ball.AXIS_X) return True return False def update(self,delay): x,y = self.loc halfHeight = self.size1/2 toMove = delay * self.speed moveY = self.dy * toMove newY = y + moveY if(newY < halfHeight or newY > self.maxY - halfHeight): return self.loc = (self.loc0,newY) def keyEvent(self,key,unicode, pressed): if(key K_UP): self.dy = -1 elif(key K_DOWN): self.dy = 1 if(not pressed): self.dy = 0 def paint(self,screen): topLeftX = self.loc0 - (self.size0 / 2) topLeftY = self.loc1 - (self.size1 / 2) rect = self.size[0, self.size1] pygame.draw.rect(screen, (255,255,255), rect) class AIPaddle(Paddle): def __init__(self,loc,size,maxY,ball,speed=125): Paddle.__init__(self,loc,size,maxY,speed) self.ball = ball def keyEvent(self,key,unicode,pressed): pass def update(self,delay): Paddle.update(self,delay) if(self.ball.loc1 > self.loc1 + 5): self.dy = 1 elif(self.ball.loc1 < self.loc1 - 1): self.dy = -1 else: self.dy = 0 class PlayingGameState(GuiState): def __init__(self,driver,screen): GuiState.__init__(self,driver,screen) self.ball = Ball(None, (640,480)) self.ball.center() self.add(self.ball) self.player1 = Paddle( (10,0), (15,75), 480) self.player1.center() self.add(self.player1) self.score1 = Score((20,5)) self.add(self.score1) self.player2 = AIPaddle( (630,0), (15,75), 480,self.ball) self.player2.center() self.add(self.player2) self.score2 = Score((610,5)) self.add(self.score2) def update(self,delay): GuiState.update(self,delay) self.player1.collidesWithBall(self.ball) self.player2.collidesWithBall(self.ball) score = 0 if(self.ball.outOfBounds < 0): score = self.score2.getScore() + 1 self.score2.setScore(score) elif(self.ball.outOfBounds > 0): score = self.score1.getScore() + 1 self.score1.setScore(score) if(score): self.ball.center() if(score >= 3): done = GameOver(self._driver,self.screen, self.score1,self.score2) self._driver.replace(done) done.py import pygame from pygame.constants import * from states import * class GameOver(State): def __init__(self,driver,screen,score1,score2): State.__init__(self,driver,screen) if(score1.getScore() > score2.getScore()): win = 1 else: win = 0 self.messageFont = pygame.font.Font(None,36) self.font = pygame.font.Font(None, 20) if(win): self.setMessage("You are victorious!") else: self.setMessage("You have lost!") self.score1 = score1 self.score2 = score2 def setMessage(self, message): self.message = message self.msgImage = self.messageFont.render(message, 0, (255,255,255)) def paint(self,screen): w = screen.get_width() h = screen.get_height() surface = self.msgImage centerX = w/2 - surface.get_width()/2 centerY = h*0.25 - surface.get_height()/2 screen.blit(surface, (centerX,centerY)) surface = self.messageFont.render("to",0,(255,255,255)) centerX = w/2 - surface.get_width()/2 centerY = h/2 - surface.get_height()/2 screen.blit(surface, (centerX,centerY)) self.score1.loc = 30,centerY self.score2.loc = 600,centerY self.score1.paint(screen) self.score2.paint(screen) =Related Links= Pygame Pychecker category:Python category:Pygame category:Tutorial